package opengl.renderer;

import java.awt.Font;
import java.awt.Rectangle;
import java.awt.font.GlyphVector;
import java.util.LinkedHashMap;
import java.util.Map.Entry;

import opengl.texture.Texture;
import opengl.texture.TextureImpl;
import color.GLColor;
import color.GLColors;
import font.GLFontUnicode;
import font.Glyph;
import font.GlyphPage;
import font.Image;

public class GLRendererFontUnicode extends GLRendererImmediate{
	private static final DisplayList EMPTY_DISPLAY_LIST = new DisplayList();
	private boolean displayListCaching = true;
	private static final int DISPLAY_LIST_CACHE_SIZE = 200;
	private int eldestDisplayListID;
	private int baseDisplayListID = -1;
	
	public final LinkedHashMap<String, DisplayList> displayLists = new LinkedHashMap<String, DisplayList>(DISPLAY_LIST_CACHE_SIZE, 1, true) {
		private static final long serialVersionUID = 1L;
		protected boolean removeEldestEntry (Entry<String, DisplayList>eldest) {
			DisplayList displayList = eldest.getValue();
			if (displayList != null) {
				eldestDisplayListID = displayList.ID;
			}
			return size() > DISPLAY_LIST_CACHE_SIZE;
		}
	};

	
	public DisplayList drawDisplayList (GLFontUnicode glFont, float x, float y, String text, GLColor color, int startIndex, int endIndex) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");
		if (text.length() == 0) return EMPTY_DISPLAY_LIST;
		if (color == null) throw new IllegalArgumentException("color cannot be null.");

		x -= glFont.getPaddingLeft();
		y -= glFont.getPaddingTop();

		String displayListKey = text.substring(startIndex, endIndex);

		glColor(color);
		TextureImpl.bindNone();

		DisplayList displayList = null;
		if (displayListCaching && glFont.queuedGlyphs.isEmpty()) {
			if (baseDisplayListID == -1) {
				baseDisplayListID = glGenLists(DISPLAY_LIST_CACHE_SIZE);
				if (baseDisplayListID == 0) {
					baseDisplayListID = -1;
					displayListCaching = false;
					return new DisplayList();
				}
			}
			// Try to use a display list compiled for this text.
			displayList = (DisplayList)displayLists.get(displayListKey);
			if (displayList != null) {
				if (displayList.invalid)
					displayList.invalid = false;
				else {
					glTranslatef(x, y, 0);
					glCallList(displayList.ID);
					glTranslatef(-x, -y, 0);
					return displayList;
				}
			} else if (displayList == null) {
				// Compile a new display list.
				displayList = new DisplayList();
				int displayListCount = displayLists.size();
				displayLists.put(displayListKey, displayList);
				if (displayListCount < DISPLAY_LIST_CACHE_SIZE)
					displayList.ID = baseDisplayListID + displayListCount;
				else
					displayList.ID = eldestDisplayListID;
			}
			displayLists.put(displayListKey, displayList);
		}

		glTranslatef(x, y, 0);

		if (displayList != null) glNewList(displayList.ID, GLConst.GL_COMPILE_AND_EXECUTE);

		char[] chars = text.substring(0, endIndex).toCharArray();
		GlyphVector vector = glFont.getFont().layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);

		int maxWidth = 0, totalHeight = 0, lines = 0;
		int extraX = 0, extraY = glFont.ascent;
		boolean startNewLine = false;
		Texture lastBind = null;
		for (int glyphIndex = 0, n = vector.getNumGlyphs(); glyphIndex < n; glyphIndex++) {
			int charIndex = vector.getGlyphCharIndex(glyphIndex);
			if (charIndex < startIndex) continue;
			if (charIndex > endIndex) break;

			int codePoint = text.codePointAt(charIndex);

			Rectangle bounds = glFont.getGlyphBounds(vector, glyphIndex, codePoint);
			Glyph glyph = glFont.getGlyph(vector.getGlyphCode(glyphIndex), codePoint, bounds, vector, glyphIndex);

			if (startNewLine && codePoint != '\n') {
				extraX = -bounds.x;
				startNewLine = false;
			}

			Image image = glyph.getImage();
			if (image == null && glFont.missingGlyph != null && glyph.isMissing()) image = glFont.missingGlyph.getImage();
			if (image != null) {
				// Draw glyph, only binding a new glyph page texture when necessary.
				Texture texture = image.getTexture();
				if (lastBind != null && lastBind != texture) {
					glEnd();
					lastBind = null;
				}
				if (lastBind == null) {
					texture.bind();
					glBegin(GLConst.GL_QUADS);
					lastBind = texture;
				}
				image.drawEmbedded(bounds.x + extraX, bounds.y + extraY, image.getWidth(), image.getHeight());
			}

			if (glyphIndex >= 0) extraX += glFont.paddingRight + glFont.getPaddingLeft() + glFont.getPaddingAdvanceX();
			maxWidth = Math.max(maxWidth, bounds.x + extraX + bounds.width);
			totalHeight = Math.max(totalHeight, glFont.ascent + bounds.y + bounds.height);

			if (codePoint == '\n') {
				startNewLine = true; // Mac gives -1 for bounds.x of '\n', so use the bounds.x of the next glyph.
				extraY += glFont.getLineHeight();
				lines++;
				totalHeight = 0;
			}
		}
		if (lastBind != null) glEnd();

		if (displayList != null) {
			glEndList();
			// Invalidate the display list if it had glyphs that need to be loaded.
			if (!glFont.queuedGlyphs.isEmpty()) displayList.invalid = true;
		}

		glTranslatef(-x, -y, 0);

		if (displayList == null) displayList = new DisplayList();
		displayList.width = (short)maxWidth;
		displayList.height = (short)(lines * glFont.getLineHeight() + totalHeight);
		return displayList;
	}

	public void drawString (GLFontUnicode font,float x, float y, String text, GLColor color, int startIndex, int endIndex) {
		drawDisplayList(font,x, y, text, color, startIndex, endIndex);
	}

	public void drawString (GLFontUnicode font,float x, float y, String text) {
		drawString(font,x, y, text, GLColors.WHITE);
	}

	public void drawString (GLFontUnicode font,float x, float y, String text, GLColor col) {
		drawString(font,x, y, text, col, 0, text.length());
	}
	
	public int getWidth (GLFontUnicode glFont, String text) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");
		if (text.length() == 0) return 0;

		if (displayListCaching) {
			DisplayList displayList = (DisplayList)displayLists.get(text);
			if (displayList != null) return displayList.width;
		}

		char[] chars = text.toCharArray();
		GlyphVector vector = glFont.getFont().layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);

		int width = 0;
		int extraX = 0;
		boolean startNewLine = false;
		for (int glyphIndex = 0, n = vector.getNumGlyphs(); glyphIndex < n; glyphIndex++) {
			int charIndex = vector.getGlyphCharIndex(glyphIndex);
			int codePoint = text.codePointAt(charIndex);
			Rectangle bounds = glFont.getGlyphBounds(vector, glyphIndex, codePoint);

			if (startNewLine && codePoint != '\n') extraX = -bounds.x;

			if (glyphIndex > 0) extraX += glFont.paddingLeft + glFont.paddingRight + glFont.paddingAdvanceX;
			width = Math.max(width, bounds.x + extraX + bounds.width);

			if (codePoint == '\n') startNewLine = true;
		}

		return width;
	}

	public int getHeight (GLFontUnicode glFont, String text) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");
		if (text.length() == 0) return 0;

		if (displayListCaching) {
			DisplayList displayList = (DisplayList)displayLists.get(text);
			if (displayList != null) return displayList.height;
		}

		char[] chars = text.toCharArray();
		GlyphVector vector = glFont.getFont().layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);

		int lines = 0, height = 0;
		for (int i = 0, n = vector.getNumGlyphs(); i < n; i++) {
			int charIndex = vector.getGlyphCharIndex(i);
			int codePoint = text.codePointAt(charIndex);
			if (codePoint == ' ') continue;
			Rectangle bounds = glFont.getGlyphBounds(vector, i, codePoint);

			height = Math.max(height, glFont.ascent + bounds.y + bounds.height);

			if (codePoint == '\n') {
				lines++;
				height = 0;
			}
		}
		return lines * glFont.getLineHeight() + height;
	}

	public int getYOffset (GLFontUnicode glFont,String text) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");

		DisplayList displayList = null;
		if (displayListCaching) {
			displayList = (DisplayList)displayLists.get(text);
			if (displayList != null && displayList.yOffset != null) return displayList.yOffset.intValue();
		}

		int index = text.indexOf('\n');
		if (index != -1) text = text.substring(0, index);
		char[] chars = text.toCharArray();
		GlyphVector vector = glFont.getFont().layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
		int yOffset = glFont.ascent + vector.getPixelBounds(null, 0, 0).y;

		if (displayList != null) displayList.yOffset = new Short((short)yOffset);

		return yOffset;
	}

	//------------------------------------------------------------inner class
	public static class DisplayList {
		boolean invalid;
		int ID;
		Short yOffset;
		public short width;
		public short height;
		public Object userData;
		DisplayList () {
		}
	}
}
